#include "Test.h"
#include "Disk.h"
#include "DiskManager.h"
#include "FileSystemCore.h"
#include "DirFileManager.h"
#include "debug.h"
#include "Exception.h"

#include <iostream>
#include <filesystem>
#include <random>

using namespace std;
namespace FS = filesystem;

uint64_t Test::calc_dirfile_pages(uint64_t NFiles)
{
	uint64_t pages1 = (NFiles + FileSystemCore::MAX_DIR_FILE_ITEM_PER_PAGE - 1) / FileSystemCore::MAX_DIR_FILE_ITEM_PER_PAGE;
	uint64_t pages2 = (pages1 + FileSystemCore::MAX_DIR_FILE_PAGE_ADDR_PER_PAGE - 1) / FileSystemCore::MAX_DIR_FILE_PAGE_ADDR_PER_PAGE;
	return pages1 + pages2;
}

bool Test::test_disk_create_open_close()
{
	try {
		vector<string> filePaths = {
			"testa.disk",
			"test/testb.disk",
			"test/test/testc.disk",
		};

		Disk disk;
		
		for(int i = 0; i < filePaths.size(); i++) {
			disk.create(filePaths[i], Const::DEFAULT_MAX_DISK_PAGE * (i + 1));
			disk.close();
		}

		for(int i = 0; i < filePaths.size(); i++) {
			disk.open(filePaths[i]);
			if(disk.getCurPage() != 0)
				throw Exception("Disk" + to_string(i + 1) + " cur page is not 0.");
			if(disk.getMaxPage() != Const::DEFAULT_MAX_DISK_PAGE * (i + 1))
				throw Exception("Disk" + to_string(i + 1) + " max page is not " + to_string(Const::DEFAULT_MAX_DISK_PAGE * (i + 1)) + ".");
			disk.close();
		}

		for(auto filePath : filePaths) {
			FS::remove(filePath);
		}
		FS::remove_all("test");

		Tool::printSuccess("Test disk create open close passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk create open close failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_disk_read_write()
{
	try {
		FS::path filePath = "testa.disk";

		vector<char*> buffers;

		for(int i = 0; i < 256; i++) {
			char* buffer = new char[Const::PAGE_SIZE];
			memset(buffer, (uint8_t)i, Const::PAGE_SIZE);
			buffers.push_back(buffer);
		}

		for(int k = 0; k < 256; k += 2) {
			char *buffer = new char[Const::PAGE_SIZE];
			int index = 0;
			for(int c1 = k; c1 < k + 2; c1++) {
				for(int c2 = 0; c2 < 256; c2++) {
					buffer[index++] = (uint8_t)c1;
					buffer[index++] = (uint8_t)c2;
				}
			}
			buffers.push_back(buffer);
		}

		Disk disk;
		disk.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);

		for(int i = 0; i < buffers.size(); i++) {
			disk.write(i, buffers[i]);
		}
		disk.close();

		disk.open(filePath);
		char buffer[Const::PAGE_SIZE];
		for(int i = 0; i < buffers.size(); i++) {
			disk.read(i, buffer);
			if (memcmp(buffer, buffers[i], Const::PAGE_SIZE) != 0) {
				for(auto p : buffers) {
					delete[] p;
				}
				throw Exception("Read disk data after reopen is not matched.");
			}
		}
		disk.close();

		for(auto p : buffers) {
			delete[] p;
		}

		FS::remove(filePath);
		Tool::printSuccess("Test disk read write passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk read write failed: " + string(e.what()));
		return false;
	}
}

void Test::test_disk()
{
	int passed = 0, total = 0;
	total++; passed += test_disk_create_open_close() ? 1 : 0;
	total++; passed += test_disk_read_write() ? 1 : 0;

	if(total == passed)
		Tool::printSuccess(std::format("Test disk passed. {}/{}.", passed, total));
	else
		Tool::printError(std::format("Test disk failed. {}/{}.", passed, total));
}

bool Test::test_disk_manager_create_open_close()
{
	try {
		vector<string> filePaths = {
			"testa.disk",
			"test/testb.disk",
			"test/test/testc.disk",
		};

		DiskManager DM;
		
		for(int i = 0; i < filePaths.size(); i++) {
			DM.create(filePaths[i], Const::DEFAULT_MAX_DISK_PAGE * (i + 1));
		}

		for(int i = 0; i < filePaths.size(); i++) {
			DM.open(filePaths[i]);
			if(DM.getUsedPage() != 4)
				throw Exception("Disk" + to_string(i + 1) + " used page is not 4.");
			if(DM.getMaxPage() != Const::DEFAULT_MAX_DISK_PAGE * (i + 1))
				throw Exception("Disk" + to_string(i + 1) + " max page is not " + to_string(Const::DEFAULT_MAX_DISK_PAGE * (i + 1)) + ".");
			DM.close();
		}
		
		for(auto filePath : filePaths) {
			FS::remove(filePath);
		}
		FS::remove_all("test");

		Tool::printSuccess("Test disk manager create open close passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk manager create open close failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_disk_manager_page_alloc_free()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);
		DM.open(filePath);

		const int NPages = 400;

		vector<uint64_t> unused;
		for(int i = 4; i < NPages + 4; i++) {
			uint64_t pageID = DM.allocPage();
			if(pageID != i)
				throw Exception("First alloc page failed.");
		}
		for(int i = 4; i < NPages + 4; i++) {
			DM.freePage(i);
			unused.push_back(i);
		}

		for(int i = 0; i < NPages; i++) {
			uint64_t pageID = DM.allocPage();
			if(pageID != unused.back())
				throw Exception("Second alloc page failed.");
			unused.pop_back();
		}
		for(int i = 100; i <= 300; i++) {
			DM.freePage(i);
			unused.push_back(i);
		}

		for(int i = 100; i <= 300; i++) {
			uint64_t pageID = DM.allocPage();
			if(pageID != unused.back())
				throw Exception("Third alloc page failed.");
			unused.pop_back();
		}
		for(int i = 50; i <= 350; i++) {
			DM.freePage(i);
			unused.push_back(i);
		}

		DM.close();
		DM.open(filePath);
		for(int i = 50; i <= 350; i++) {
			uint64_t pageID = DM.allocPage();
			if(pageID != unused.back())
				throw Exception("Fourth alloc page failed.");
			unused.pop_back();
		}

		DM.close();
		FS::remove(filePath);
		Tool::printSuccess("Test disk manager page alloc free passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk manager page alloc free failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_disk_manager_inode_alloc_free()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);
		DM.open(filePath);

		const int NINodes = 2400;

		vector<uint64_t> unused;
		for(int i = 1; i < NINodes + 1; i++) {
			uint64_t iNodeID = DM.allocINodeID();
			if(iNodeID != i)
				throw Exception("First alloc inode failed.");
		}
		for(int i = 1; i < NINodes + 1; i++) {
			DM.freeINodeID(i);
			unused.push_back(i);
		}

		for(int i = 0; i < NINodes; i++) {
			uint64_t iNodeID = DM.allocINodeID();
			if(iNodeID != unused.back())
				throw Exception("Second alloc inode failed.");
			unused.pop_back();
		}
		for(int i = 600; i <= 2200; i++) {
			DM.freeINodeID(i);
			unused.push_back(i);
		}

		for(int i = 600; i <= 2200; i++) {
			uint64_t iNodeID = DM.allocINodeID();
			if(iNodeID != unused.back())
				throw Exception("Third alloc inode failed.");
			unused.pop_back();
		}
		for(int i = 1600; i <= 2400; i++) {
			DM.freeINodeID(i);
			unused.push_back(i);
		}

		DM.close();
		DM.open(filePath);
		for(int i = 1600; i <= 2400; i++) {
			uint64_t iNodeID = DM.allocINodeID();
			if(iNodeID != unused.back())
				throw Exception("Fourth alloc inode failed.");
			unused.pop_back();
		}

		DM.close();
		FS::remove(filePath);
		Tool::printSuccess("Test disk manager inode alloc free passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk manager inode alloc free failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_disk_manager_set_get_inode()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);
		DM.open(filePath);

		const int NINodes = 2400;

		mt19937_64 random_engine;
		uniform_int_distribution<uint64_t> distrubution(0, numeric_limits<uint64_t>::max());

		vector<pair<uint64_t, INode>> INodes;
		for(int i = 0; i < NINodes; i++) {
			INode iNode;
			iNode.accessTime = distrubution(random_engine);
			iNode.createTime = distrubution(random_engine);
			iNode.modifyTime = distrubution(random_engine);
			iNode.addr = distrubution(random_engine);
			iNode.size = distrubution(random_engine);
			iNode.pages = distrubution(random_engine);
			iNode.parentID = distrubution(random_engine);
			iNode.systemID = (uint32_t)distrubution(random_engine);
			iNode.typeMode = (uint16_t)distrubution(random_engine);
			iNode.groupID = (uint8_t)distrubution(random_engine);
			iNode.userID = (uint8_t)distrubution(random_engine);
			INodes.push_back(make_pair(DM.allocINodeID(), iNode));
		}
		for(auto &[ID, node] : INodes) {
			DM.setINode(ID, node);
		}
		for(auto &[ID, node] : INodes) {
			INode tmp = DM.getINode(ID);
			if(memcmp(&tmp, &node, sizeof(INode)) != 0)
				throw Exception("Get iNode failed.");
		}

		DM.close();
		DM.open(filePath);

		for(auto &[ID, node] : INodes) {
			INode tmp = DM.getINode(ID);
			if(memcmp(&tmp, &node, sizeof(INode)) != 0)
				throw Exception("Get iNode after reopen failed.");
		}

		DM.close();
		FS::remove(filePath);
		Tool::printSuccess("Test disk manager set get inode passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk manager set get inode failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_disk_manager_read_write()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);
		DM.open(filePath);

		vector<pair<uint64_t, char*>> buffers;

		for(int i = 0; i < 256; i++) {
			char* buffer = new char[Const::PAGE_SIZE];
			memset(buffer, (uint8_t)i, Const::PAGE_SIZE);
			buffers.push_back(make_pair(DM.allocPage(), buffer));
		}

		for(int k = 0; k < 256; k += 2) {
			char *buffer = new char[Const::PAGE_SIZE];
			int index = 0;
			for(int c1 = k; c1 < k + 2; c1++) {
				for(int c2 = 0; c2 < 256; c2++) {
					buffer[index++] = (uint8_t)c1;
					buffer[index++] = (uint8_t)c2;
				}
			}
			buffers.push_back(make_pair(DM.allocPage(), buffer));
		}

		for(auto &[pageID, data] : buffers) {
			DM.write(pageID, data);
		}
		DM.close();

		DM.open(filePath);
		for(auto &[pageID, data] : buffers) {
			char buffer[Const::PAGE_SIZE];
			DM.read(pageID, buffer);
			if(memcmp(buffer, data, Const::PAGE_SIZE) != 0)
			{
				for(int i = 0; i < Const::PAGE_SIZE; i++) {
					delete[] buffers[i].second;
				}
				throw Exception("Read page after reopen failed.");
			}
		}
		DM.close();

		for(auto &[pageID, data] : buffers) {
			delete[] data;
		}
		FS::remove(filePath);
		Tool::printSuccess("Test disk manager read write passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test disk manager read write failed: " + string(e.what()));
		return false;
	}
}

void Test::test_disk_manager()
{
	int passed = 0, total = 0;
	total++; passed += test_disk_manager_create_open_close() ? 1 : 0;
	total++; passed += test_disk_manager_page_alloc_free() ? 1 : 0;
	total++; passed += test_disk_manager_inode_alloc_free() ? 1 : 0;
	total++; passed += test_disk_manager_set_get_inode() ? 1 : 0;
	total++; passed += test_disk_manager_read_write() ? 1 : 0;

	if(total == passed)
		Tool::printSuccess(std::format("Test disk manager passed. {}/{}.", passed, total));
	else
		Tool::printError(std::format("Test disk manager failed. {}/{}.", passed, total));
}

bool Test::test_dirfile_manager_append_readall_query()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);

		FileSystemCore FSC;
		FSC.open(filePath,{}, true);

		FileSystemCore::DirFileManager DFM(FSC);
		DFM.open(FSC.curAbsDir);

		const int NFiles = 3200;
		vector<FileSystemCore::DirNode> files;
		for(int i = 0; i < NFiles; i++) {
			FileSystemCore::DirNode newNode;
			newNode.iNode.initDirectory(FSC.systemID, FSC.groupID, FSC.userID);
			newNode.iNode.parentID = FSC.curAbsDir.back().iNodeID;
			newNode.iNodeID = FSC.DMs[0].allocINodeID();
			newNode.name = "f" + to_string(i);
			files.push_back(newNode);
		}
		uint64_t diskUsed1 = FSC.DMs[0].getUsedPage();

		for(int i = 0; i < files.size(); i++) {
			DFM.append(files[i].name, files[i].iNodeID, files[i].iNode);
		}
		FSC.curAbsDir.back().iNode = DFM.getINode();
		if(FSC.curAbsDir.back().iNode.pages != Test::calc_dirfile_pages(files.size())) {
			throw Exception("Append dirfile pages is incorrect.");
		}
		if(FSC.curAbsDir.back().iNode.pages != FSC.DMs[0].getUsedPage() - diskUsed1) {
			throw Exception("Append disk pages is incorrect.");
		}

		DFM.close();
		DFM.open(FSC.curAbsDir);

		FileSystemCore::SimpleDirFileList result;
		DFM.readAll(result);
		for(int i = 0; i < files.size(); i++) {
			if(result[i].name != files[i].name || result[i].iNodeID != files[i].iNodeID || result[i].itemID != i) {
				throw Exception("Read all result is incorrect.");
			}
		}

		for(int i = 0; i < files.size(); i += 20) {
			FileSystemCore::SimpleDirFileItem result;
			DFM.query(i, result);
			if(result.name != files[i].name || result.iNodeID != files[i].iNodeID || result.itemID != i) {
				throw Exception("Query result is incorrect.");
			}
		}

		FSC.close();
		FS::remove(filePath);
		Tool::printSuccess("Test dirfile manager append readall query passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test dirfile manager append readall query failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_dirfile_manager_find_exist()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);

		FileSystemCore FSC;
		FSC.open(filePath,{}, true);

		FileSystemCore::DirFileManager DFM(FSC);
		DFM.open(FSC.curAbsDir);

		const int NFiles = 3200;
		vector<FileSystemCore::DirNode> files;
		for(int i = 0; i < NFiles; i++) {
			FileSystemCore::DirNode newNode;
			newNode.iNode.initDirectory(FSC.systemID, FSC.groupID, FSC.userID);
			newNode.iNode.parentID = FSC.curAbsDir.back().iNodeID;
			newNode.iNodeID = FSC.DMs[0].allocINodeID();
			newNode.name = "f" + to_string(i);
			files.push_back(newNode);
		}

		for(int i = 0; i < files.size(); i++) {
			DFM.append(files[i].name, files[i].iNodeID, files[i].iNode);
		}

		for(int i = 0; i < files.size(); i += 20) {
			FileSystemCore::SimpleDirFileItem result;
			DFM.find(files[i].name, FileSystemCore::DIR, result);
			if(result.name != files[i].name || result.iNodeID != files[i].iNodeID || result.itemID != i) {
				throw Exception("Find existed result is incorrect.");
			}
		}

		{
			FileSystemCore::SimpleDirFileItem result;
			DFM.find("fnone", FileSystemCore::DIR, result);
			if(result.isExist()) {
				throw Exception("Find not existed result is incorrect.");
			}
		}

		for(int i = 0; i < files.size(); i += 20) {
			if(!DFM.exist(files[i].name, FileSystemCore::DIR)) {
				throw Exception("Exist existed result is incorrect.");
			}
		}

		{
			if(DFM.exist("fnone", FileSystemCore::DIR)) {
				throw Exception("Exist not existed result is incorrect.");
			}
		}

		FSC.close();
		FS::remove(filePath);
		Tool::printSuccess("Test dirfile manager find exist passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test dirfile manager find exist failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_dirfile_manager_swap_popback()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);

		FileSystemCore FSC;
		FSC.open(filePath,{}, true);

		FileSystemCore::DirFileManager DFM(FSC);
		DFM.open(FSC.curAbsDir);

		const int NFiles = 3200;
		vector<FileSystemCore::DirNode> files;
		for(int i = 0; i < NFiles; i++) {
			FileSystemCore::DirNode newNode;
			newNode.iNode.initDirectory(FSC.systemID, FSC.groupID, FSC.userID);
			newNode.iNode.parentID = FSC.curAbsDir.back().iNodeID;
			newNode.iNodeID = FSC.DMs[0].allocINodeID();
			newNode.name = "f" + to_string(i);
			files.push_back(newNode);
		}
		uint64_t diskUsed1 = FSC.DMs[0].getUsedPage();

		for(int i = 0; i < files.size(); i++) {
			DFM.append(files[i].name, files[i].iNodeID, files[i].iNode);
		}

		for(int i = 0, j = (int)files.size() - 1; i < 2000; i += 20, j -= 2) {
			DFM.swap(i, j);
			swap(files[i], files[j]);
		}

		FileSystemCore::SimpleDirFileList result;
		DFM.readAll(result);
		for(int i = 0; i < files.size(); i++) {
			if(result[i].name != files[i].name || result[i].iNodeID != files[i].iNodeID || result[i].itemID != i) {
				throw Exception("Swap result is incorrect.");
			}
		}

		int half = (int)files.size() / 2;
		for(int i = 0; i < half; i++) {
			DFM.popback();
			files.pop_back();
		}
		FSC.curAbsDir.back().iNode = DFM.getINode();
		if(FSC.curAbsDir.back().iNode.pages != Test::calc_dirfile_pages(files.size())) {
			throw Exception("Popback dirfile pages is incorrect.");
		}
		if(FSC.curAbsDir.back().iNode.pages != FSC.DMs[0].getUsedPage() - diskUsed1) {
			throw Exception("Popback disk pages is incorrect.");
		}

		DFM.close();
		DFM.open(FSC.curAbsDir);

		DFM.readAll(result);
		for(int i = 0; i < files.size(); i++) {
			if(result[i].name != files[i].name || result[i].iNodeID != files[i].iNodeID || result[i].itemID != i) {
				throw Exception("Popback result is incorrect.");
			}
		}
		for(int i = 0; i < files.size(); i++) {
			DFM.popback();
		}
		FSC.curAbsDir.back().iNode = DFM.getINode();
		if(FSC.curAbsDir.back().iNode.pages != Test::calc_dirfile_pages(0)) {
			throw Exception("Popback all dirfile pages is incorrect.");
		}
		if(FSC.curAbsDir.back().iNode.pages != FSC.DMs[0].getUsedPage() - diskUsed1) {
			throw Exception("Popback all disk pages is incorrect.");
		}

		DFM.close();
		DFM.open(FSC.curAbsDir);

		DFM.readAll(result);
		if(result.size() != 0) {
			throw Exception("Popback result is incorrect.");
		}

		FSC.close();
		FS::remove(filePath);
		Tool::printSuccess("Test dirfile manager swap popback passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test dirfile manager swap popback failed: " + string(e.what()));
		return false;
	}
}

bool Test::test_dirfile_manager_update_removeall()
{
	try {
		FS::path filePath = "testa.disk";

		DiskManager DM;
		DM.create(filePath, Const::DEFAULT_MAX_DISK_PAGE);

		FileSystemCore FSC;
		FSC.open(filePath,{}, true);

		FileSystemCore::DirFileManager DFM(FSC);
		DFM.open(FSC.curAbsDir);

		const int NFiles = 3200;
		vector<FileSystemCore::DirNode> files;
		for(int i = 0; i < NFiles; i++) {
			FileSystemCore::DirNode newNode;
			newNode.iNode.initDirectory(FSC.systemID, FSC.groupID, FSC.userID);
			newNode.iNode.parentID = FSC.curAbsDir.back().iNodeID;
			newNode.iNodeID = FSC.DMs[0].allocINodeID();
			newNode.name = "f" + to_string(i);
			files.push_back(newNode);
		}
		uint64_t diskUsed1 = FSC.DMs[0].getUsedPage();

		for(int i = 0; i < files.size(); i++) {
			DFM.append(files[i].name, files[i].iNodeID, files[i].iNode);
		}

		for(int i = 0; i < files.size(); i += 20) {
			files[i].name = "rf" + to_string(i);
			DFM.update(i, files[i].name, files[i].iNodeID, files[i].iNode);
		}

		FileSystemCore::SimpleDirFileList result;
		DFM.readAll(result);
		for(int i = 0; i < files.size(); i++) {
			if(result[i].name != files[i].name || result[i].iNodeID != files[i].iNodeID || result[i].itemID != i) {
				throw Exception("Update result is incorrect.");
			}
		}

		DFM.removeAll();
		FSC.curAbsDir.back().iNode = DFM.getINode();
		if(FSC.curAbsDir.back().iNode.pages != Test::calc_dirfile_pages(0)) {
			throw Exception("Remove all dirfile pages is incorrect.");
		}
		if(FSC.curAbsDir.back().iNode.pages != FSC.DMs[0].getUsedPage() - diskUsed1) {
			throw Exception("Remove all disk pages is incorrect.");
		}

		DFM.close();
		DFM.open(FSC.curAbsDir);

		DFM.readAll(result);
		if(result.size() != 0) {
			throw Exception("Removeall result is incorrect.");
		}

		FSC.close();
		FS::remove(filePath);
		Tool::printSuccess("Test dirfile manager update removeall passed.");
		return true;
	} catch (exception &e) {
		Tool::printError("Test dirfile manager update removeall failed: " + string(e.what()));
		return false;
	}
}

void Test::test_dirfile_manager()
{
	int passed = 0, total = 0;
	total++; passed += test_dirfile_manager_append_readall_query() ? 1 : 0;
	total++; passed += test_dirfile_manager_find_exist() ? 1 : 0;
	total++; passed += test_dirfile_manager_swap_popback() ? 1 : 0;
	total++; passed += test_dirfile_manager_update_removeall() ? 1 : 0;

	if(total == passed)
		Tool::printSuccess(std::format("Test dirfile manager passed. {}/{}.", passed, total));
	else
		Tool::printError(std::format("Test dirfile manager failed. {}/{}.", passed, total));
}
